// Copyright (c) 2024 CHANGLEI. All rights reserved.

import 'dart:math' as math;

import 'package:charts_grasp/src/chart/axis/axis.dart';
import 'package:charts_grasp/src/chart/axis/multi_level_labels.dart';
import 'package:charts_grasp/src/chart/chart_series/series_renderer_properties.dart';
import 'package:charts_grasp/src/chart/chart_series/xy_data_series.dart';
import 'package:charts_grasp/src/chart/common/cartesian_state_properties.dart';
import 'package:charts_grasp/src/chart/common/common.dart' show updateErrorBarAxisRange;
import 'package:charts_grasp/src/chart/utils/helper.dart';
import 'package:charts_grasp/src/common/event_args.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart' show NumberFormat;

@immutable
class NumericAxis extends ChartAxis {
  NumericAxis({
    super.name,
    super.isVisible,
    super.anchorRangeToVisiblePoints,
    super.title,
    super.axisLine,
    super.rangePadding,
    super.labelIntersectAction,
    super.labelRotation,
    this.labelFormat,
    this.numberFormat,
    super.labelAlignment,
    super.labelPosition,
    super.tickPosition,
    super.isInversed,
    super.opposedPosition,
    super.minorTicksPerInterval,
    super.maximumLabels,
    super.majorTickLines,
    super.minorTickLines,
    super.majorGridLines,
    super.minorGridLines,
    super.edgeLabelPlacement,
    super.labelStyle,
    super.plotOffset,
    super.zoomFactor,
    super.zoomPosition,
    super.enableAutoIntervalOnZooming,
    super.interactiveTooltip,
    this.minimum,
    this.maximum,
    super.interval,
    this.visibleMinimum,
    this.visibleMaximum,
    super.crossesAt,
    super.associatedAxisName,
    super.placeLabelsNearAxisLine,
    super.plotBands,
    this.decimalPlaces = 3,
    super.desiredIntervals,
    super.rangeController,
    super.maximumLabelWidth,
    super.labelsExtent,
    super.autoScrollingDelta,
    super.autoScrollingMode,
    super.borderWidth,
    super.borderColor,
    super.axisBorderType,
    List<NumericMultiLevelLabel>? super.multiLevelLabels,
    super.multiLevelLabelFormatter,
    super.multiLevelLabelStyle,
    super.axisLabelFormatter,
  });

  final String? labelFormat;

  final NumberFormat? numberFormat;

  final double? minimum;

  final double? maximum;

  final double? visibleMinimum;

  final double? visibleMaximum;

  final int decimalPlaces;

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) {
      return true;
    }
    if (other.runtimeType != runtimeType) {
      return false;
    }

    return other is NumericAxis &&
        other.name == name &&
        other.isVisible == isVisible &&
        other.title == title &&
        other.axisLine == axisLine &&
        other.numberFormat == numberFormat &&
        other.labelFormat == labelFormat &&
        other.rangePadding == rangePadding &&
        other.decimalPlaces == decimalPlaces &&
        other.edgeLabelPlacement == edgeLabelPlacement &&
        other.labelPosition == labelPosition &&
        other.tickPosition == tickPosition &&
        other.labelRotation == labelRotation &&
        other.labelIntersectAction == labelIntersectAction &&
        other.labelAlignment == labelAlignment &&
        other.isInversed == isInversed &&
        other.opposedPosition == opposedPosition &&
        other.minorTicksPerInterval == minorTicksPerInterval &&
        other.maximumLabels == maximumLabels &&
        other.majorTickLines == majorTickLines &&
        other.minorTickLines == minorTickLines &&
        other.majorGridLines == majorGridLines &&
        other.minorGridLines == minorGridLines &&
        other.labelStyle == labelStyle &&
        other.plotOffset == plotOffset &&
        other.zoomFactor == zoomFactor &&
        other.zoomPosition == zoomPosition &&
        other.interactiveTooltip == interactiveTooltip &&
        other.minimum == minimum &&
        other.maximum == maximum &&
        other.interval == interval &&
        other.visibleMinimum == visibleMinimum &&
        other.visibleMaximum == visibleMaximum &&
        other.crossesAt == crossesAt &&
        other.associatedAxisName == associatedAxisName &&
        other.placeLabelsNearAxisLine == placeLabelsNearAxisLine &&
        other.plotBands == plotBands &&
        other.desiredIntervals == desiredIntervals &&
        other.rangeController == rangeController &&
        other.maximumLabelWidth == maximumLabelWidth &&
        other.labelsExtent == labelsExtent &&
        other.autoScrollingDelta == autoScrollingDelta &&
        other.axisBorderType == axisBorderType &&
        other.borderColor == borderColor &&
        other.borderWidth == borderWidth &&
        other.multiLevelLabelStyle == multiLevelLabelStyle &&
        other.multiLevelLabels == multiLevelLabels &&
        other.multiLevelLabelFormatter == multiLevelLabelFormatter &&
        other.autoScrollingMode == autoScrollingMode &&
        other.axisLabelFormatter == axisLabelFormatter;
  }

  @override
  int get hashCode {
    final List<Object?> values = <Object?>[
      name,
      isVisible,
      title,
      axisLine,
      numberFormat,
      labelFormat,
      rangePadding,
      decimalPlaces,
      edgeLabelPlacement,
      labelPosition,
      tickPosition,
      labelRotation,
      labelIntersectAction,
      labelAlignment,
      isInversed,
      opposedPosition,
      minorTicksPerInterval,
      maximumLabels,
      majorTickLines,
      minorTickLines,
      majorGridLines,
      minorGridLines,
      labelStyle,
      plotOffset,
      zoomFactor,
      zoomPosition,
      interactiveTooltip,
      minimum,
      maximum,
      interval,
      visibleMinimum,
      visibleMaximum,
      crossesAt,
      associatedAxisName,
      placeLabelsNearAxisLine,
      plotBands,
      desiredIntervals,
      rangeController,
      maximumLabelWidth,
      labelsExtent,
      autoScrollingDelta,
      axisBorderType,
      borderColor,
      borderWidth,
      multiLevelLabelStyle,
      multiLevelLabels,
      multiLevelLabelFormatter,
      autoScrollingMode,
      axisLabelFormatter,
    ];
    return Object.hashAll(values);
  }
}

class NumericAxisRenderer extends ChartAxisRenderer {
  NumericAxisRenderer(NumericAxis numericAxis, CartesianStateProperties stateProperties) {
    _axisDetails = NumericAxisDetails(numericAxis, this, stateProperties);
    AxisHelper.setAxisRendererDetails(this, _axisDetails);
  }

  late NumericAxisDetails _axisDetails;

  @override
  void applyRangePadding(VisibleRange range, num? interval) {
    ActualRangeChangedArgs rangeChangedArgs;
    final NumericAxisDetails axisDetails = AxisHelper.getAxisRendererDetails(this) as NumericAxisDetails;
    if (!(axisDetails.numericAxis.minimum != null && axisDetails.numericAxis.maximum != null)) {
      axisDetails.applyRangePaddings(this, axisDetails.stateProperties, range, interval!);
    }

    calculateVisibleRange(axisDetails.axisSize);

    if ((axisDetails.numericAxis.visibleMinimum != null || axisDetails.numericAxis.visibleMaximum != null) &&
        (axisDetails.numericAxis.visibleMinimum != axisDetails.numericAxis.visibleMaximum) &&
        (!axisDetails.stateProperties.isRedrawByZoomPan)) {
      axisDetails.stateProperties.isRedrawByZoomPan = false;
      axisDetails.visibleRange!.minimum =
          axisDetails.visibleMinimum ?? axisDetails.numericAxis.visibleMinimum ?? axisDetails.actualRange!.minimum;
      axisDetails.visibleRange!.maximum =
          axisDetails.visibleMaximum ?? axisDetails.numericAxis.visibleMaximum ?? axisDetails.actualRange!.maximum;
      axisDetails.visibleRange!.delta = axisDetails.visibleRange!.maximum - axisDetails.visibleRange!.minimum;
      axisDetails.visibleRange!.interval = interval == null
          ? axisDetails.calculateNumericNiceInterval(
              this, axisDetails.visibleRange!.maximum - axisDetails.visibleRange!.minimum, axisDetails.axisSize,)
          : axisDetails.visibleRange!.interval;
      axisDetails.zoomFactor = axisDetails.visibleRange!.delta / range.delta;
      axisDetails.zoomPosition = (axisDetails.visibleRange!.minimum - axisDetails.actualRange!.minimum) / range.delta;
    }
    if (axisDetails.chart.onActualRangeChanged != null) {
      rangeChangedArgs = ActualRangeChangedArgs(axisDetails.name, axisDetails.numericAxis, range.minimum,
          range.maximum, range.interval, axisDetails.orientation,);
      rangeChangedArgs.visibleMin = axisDetails.visibleRange!.minimum;
      rangeChangedArgs.visibleMax = axisDetails.visibleRange!.maximum;
      rangeChangedArgs.visibleInterval = axisDetails.visibleRange!.interval;
      axisDetails.chart.onActualRangeChanged!(rangeChangedArgs);
      axisDetails.visibleRange!.minimum = rangeChangedArgs.visibleMin;
      axisDetails.visibleRange!.maximum = rangeChangedArgs.visibleMax;
      axisDetails.visibleRange!.delta = axisDetails.visibleRange!.maximum - axisDetails.visibleRange!.minimum;
      axisDetails.visibleRange!.interval = rangeChangedArgs.visibleInterval;
      axisDetails.zoomFactor = axisDetails.visibleRange!.delta / range.delta;
      axisDetails.zoomPosition = (axisDetails.visibleRange!.minimum - axisDetails.actualRange!.minimum) / range.delta;
    }
  }

  @override
  void generateVisibleLabels() {
    final ChartAxisRendererDetails axisDetails = AxisHelper.getAxisRendererDetails(this);
    final VisibleRange? visibleRange = axisDetails.visibleRange;
    final NumericAxis numericAxis = axisDetails.axis as NumericAxis;
    num tempInterval = visibleRange!.minimum;
    String text;
    final num maximumVisibleRange = visibleRange.maximum;
    num interval = visibleRange.interval;
    interval = interval.toString().split('.').length >= 2
        ? interval.toString().split('.')[1].length == 1 && interval.toString().split('.')[1] == '0'
            ? interval.floor()
            : interval
        : interval;
    axisDetails.visibleLabels = <AxisLabel>[];
    for (; tempInterval <= maximumVisibleRange; tempInterval += interval) {
      num minimumVisibleRange = tempInterval;
      if (minimumVisibleRange <= maximumVisibleRange && minimumVisibleRange >= visibleRange.minimum) {
        final int fractionDigits = (minimumVisibleRange.toString().split('.').length >= 2)
            ? minimumVisibleRange.toString().split('.')[1].length
            : 0;
        final int fractionDigitValue = fractionDigits > 20 ? 20 : fractionDigits;
        minimumVisibleRange = minimumVisibleRange.toString().contains('e')
            ? minimumVisibleRange
            : num.tryParse(minimumVisibleRange.toStringAsFixed(fractionDigitValue))!;
        if (minimumVisibleRange.toString().split('.').length > 1) {
          final String str = minimumVisibleRange.toString();
          final List<String> list = str.split('.');
          minimumVisibleRange = double.parse(minimumVisibleRange.toStringAsFixed(numericAxis.decimalPlaces));
          if (list.length > 1 &&
              (list[1] == '0' ||
                  list[1] == '00' ||
                  list[1] == '000' ||
                  list[1] == '0000' ||
                  list[1] == '00000' ||
                  minimumVisibleRange % 1 == 0)) {
            minimumVisibleRange = minimumVisibleRange.round();
          }
        }
        text = minimumVisibleRange.toString();
        if (numericAxis.numberFormat != null) {
          text = numericAxis.numberFormat!.format(minimumVisibleRange);
        }
        if (numericAxis.labelFormat != null && numericAxis.labelFormat != '') {
          text = numericAxis.labelFormat!.replaceAll(RegExp('{value}'), text);
        }
        text = axisDetails.stateProperties.chartAxis.primaryYAxisDetails.isStack100 == true &&
                axisDetails.name == axisDetails.stateProperties.chartAxis.primaryYAxisDetails.name
            ? '$text%'
            : text;
        axisDetails.triggerLabelRenderEvent(text, tempInterval);
      }
    }

    axisDetails.calculateMaximumLabelSize(this, axisDetails.stateProperties);
    if (numericAxis.multiLevelLabels != null && numericAxis.multiLevelLabels!.isNotEmpty) {
      generateMultiLevelLabels(_axisDetails);
      calculateMultiLevelLabelBounds(axisDetails);
    }
  }

  @override
  void calculateVisibleRange(Size availableSize) {
    final ChartAxisRendererDetails axisDetails = AxisHelper.getAxisRendererDetails(this);
    final CartesianStateProperties stateProperties = axisDetails.stateProperties;
    axisDetails.setOldRangeFromRangeController();
    axisDetails.visibleRange =
        stateProperties.rangeChangeBySlider && axisDetails.rangeMinimum != null && axisDetails.rangeMaximum != null
            ? VisibleRange(axisDetails.rangeMinimum, axisDetails.rangeMaximum)
            : VisibleRange(axisDetails.actualRange!.minimum, axisDetails.actualRange!.maximum);
    axisDetails.visibleRange!.delta = axisDetails.actualRange!.delta;
    axisDetails.visibleRange!.interval = axisDetails.actualRange!.interval;
    bool canAutoScroll = false;
    if (axisDetails.axis.autoScrollingDelta != null &&
        axisDetails.axis.autoScrollingDelta! > 0 &&
        !stateProperties.isRedrawByZoomPan) {
      canAutoScroll = true;
      axisDetails.updateAutoScrollingDelta(axisDetails.axis.autoScrollingDelta!, this);
    }
    if ((!canAutoScroll || (stateProperties.zoomedState ?? false)) &&
        !(stateProperties.rangeChangeBySlider && !stateProperties.canSetRangeController)) {
      axisDetails.setZoomFactorAndPosition(this, stateProperties.zoomedAxisRendererStates);
    }
    if (axisDetails.zoomFactor < 1 ||
        axisDetails.zoomPosition > 0 ||
        (axisDetails.axis.rangeController != null && !stateProperties.renderingDetails.initialRender!) &&
            !(stateProperties.rangeChangeBySlider || !stateProperties.canSetRangeController)) {
      stateProperties.zoomProgress = true;
      axisDetails.calculateZoomRange(this, availableSize);
      axisDetails.visibleRange!.interval =
          !canAutoScroll && axisDetails.axis.enableAutoIntervalOnZooming && stateProperties.zoomProgress
              ? calculateInterval(axisDetails.visibleRange!, axisDetails.axisSize)
              : axisDetails.visibleRange!.interval;
      if (axisDetails.axis.rangeController != null &&
          stateProperties.isRedrawByZoomPan &&
          stateProperties.canSetRangeController &&
          stateProperties.zoomProgress) {
        stateProperties.rangeChangedByChart = true;
        axisDetails.setRangeControllerValues(this);
      }
    }
    axisDetails.setZoomValuesFromRangeController();
  }

  @override
  num calculateInterval(VisibleRange range, Size availableSize) {
    final ChartAxisRendererDetails axisDetails = AxisHelper.getAxisRendererDetails(this);
    return axisDetails.calculateNumericNiceInterval(this, range.maximum - range.minimum, axisDetails.axisSize);
  }
}

class NumericAxisDetails extends ChartAxisRendererDetails {
  NumericAxisDetails(
    this.numericAxis,
    ChartAxisRenderer axisRenderer,
    CartesianStateProperties stateProperties,
  ) : super(numericAxis, stateProperties, axisRenderer);

  final int axisPadding = 5;

  final int innerPadding = 5;

  final NumericAxis numericAxis;

  void findAxisMinMaxValues(
    SeriesRendererDetails seriesRendererDetails,
    CartesianChartPoint<dynamic> point,
    int pointIndex,
    int dataLength, [
    bool? isXVisibleRange,
    bool? isYVisibleRange,
  ]) {
    final bool anchorRangeToVisiblePoints = seriesRendererDetails.yAxisDetails!.axis.anchorRangeToVisiblePoints;
    final String seriesType = seriesRendererDetails.seriesType;
    point.xValue = point.x;
    point.yValue = point.y;
    if (isYVisibleRange!) {
      seriesRendererDetails.minimumX ??= point.xValue;
      seriesRendererDetails.maximumX ??= point.xValue;
    }
    if ((isXVisibleRange! || !anchorRangeToVisiblePoints) &&
        !seriesType.contains('range') &&
        !seriesType.contains('hilo') &&
        !seriesType.contains('candle') &&
        seriesType != 'boxandwhisker' &&
        seriesType != 'waterfall') {
      seriesRendererDetails.minimumY ??= point.yValue;
      seriesRendererDetails.maximumY ??= point.yValue;
    }

    if (isYVisibleRange && point.xValue != null) {
      seriesRendererDetails.minimumX = math.min(seriesRendererDetails.minimumX!, point.xValue as num);
      seriesRendererDetails.maximumX = math.max(seriesRendererDetails.maximumX!, point.xValue as num);
    }
    if (isXVisibleRange || !anchorRangeToVisiblePoints) {
      if (point.yValue != null &&
          (!seriesType.contains('range') &&
              !seriesType.contains('hilo') &&
              !seriesType.contains('candle') &&
              seriesType != 'boxandwhisker' &&
              seriesType != 'waterfall')) {
        seriesRendererDetails.minimumY = math.min(seriesRendererDetails.minimumY!, point.yValue as num);
        seriesRendererDetails.maximumY = math.max(seriesRendererDetails.maximumY!, point.yValue as num);
      }
      if (point.high != null) {
        highMin = findMinValue(highMin ?? point.high, point.high);
        highMax = findMaxValue(highMax ?? point.high, point.high);
      }
      if (point.low != null) {
        lowMin = findMinValue(lowMin ?? point.low, point.low);
        lowMax = findMaxValue(lowMax ?? point.low, point.low);
      }
      if (point.maximum != null) {
        highMin = findMinValue(highMin ?? point.maximum!, point.maximum!);
        highMax = findMaxValue(highMax ?? point.maximum!, point.maximum!);
      }
      if (point.minimum != null) {
        lowMin = findMinValue(lowMin ?? point.minimum!, point.minimum!);
        lowMax = findMaxValue(lowMax ?? point.minimum!, point.minimum!);
      }
      if (seriesType == 'waterfall') {
        point.yValue ??= 0;
        seriesRendererDetails.minimumY = findMinValue(seriesRendererDetails.minimumY ?? point.yValue, point.yValue);
        seriesRendererDetails.maximumY =
            findMaxValue(seriesRendererDetails.maximumY ?? point.maxYValue, point.maxYValue);
      }
      if (seriesType == 'errorbar') {
        updateErrorBarAxisRange(seriesRendererDetails, point);
      }
    }
    if (pointIndex >= dataLength - 1) {
      if (seriesType.contains('range') ||
          seriesType.contains('hilo') ||
          seriesType.contains('candle') ||
          seriesType == 'boxandwhisker') {
        lowMin ??= 0;
        lowMax ??= 5;
        highMin ??= 0;
        highMax ??= 5;
        seriesRendererDetails.minimumY = math.min(lowMin!, highMin!);
        seriesRendererDetails.maximumY = math.max(lowMax!, highMax!);
      }

      seriesRendererDetails.minimumX ??= 0;
      seriesRendererDetails.minimumY ??= 0;
      seriesRendererDetails.maximumX ??= 5;
      seriesRendererDetails.maximumY ??= 5;
    }
  }

  void _controlListener() {
    stateProperties.canSetRangeController = false;
    if (axis.rangeController != null && !stateProperties.rangeChangedByChart) {
      updateRangeControllerValues(this);
      stateProperties.rangeChangeBySlider = true;
      stateProperties.redrawByRangeChange();
    }
  }

  void calculateRangeAndInterval(CartesianStateProperties stateProperties, [String? type]) {
    chart = stateProperties.chart;
    if (axis.rangeController != null) {
      stateProperties.rangeChangeBySlider = true;
      axis.rangeController!.addListener(_controlListener);
    }
    final Rect containerRect = stateProperties.renderingDetails.chartContainerRect;
    final Rect rect = Rect.fromLTWH(containerRect.left, containerRect.top, containerRect.width, containerRect.height);
    axisSize = Size(rect.width, rect.height);
    axisRenderer.calculateRange(axisRenderer);
    _calculateActualRange();
    if (actualRange != null) {
      axisRenderer.applyRangePadding(actualRange!, actualRange!.interval);
      if (type == null && type != 'AxisCross' && numericAxis.isVisible) {
        axisRenderer.generateVisibleLabels();
      }
    }
  }

  void _calculateActualRange() {
    min ??= 0;
    max ??= 5;

    if (min == max && min! < 0 && max! < 0) {
      max = 0;
    }
    if (min == max && min! > 0 && max! > 0) {
      min = 0;
    }

    actualRange = VisibleRange(numericAxis.minimum ?? min, numericAxis.maximum ?? max);
    if (axis.anchorRangeToVisiblePoints &&
        needCalculateYRange(numericAxis.minimum, numericAxis.maximum, stateProperties, orientation!)) {
      actualRange = calculateYRangeOnZoomX(actualRange!, this);
    }

    if (actualRange!.minimum == actualRange!.maximum) {
      actualRange!.maximum += 1;
    } else if ((actualRange!.minimum > actualRange!.maximum) == true) {
      actualRange!.minimum = actualRange!.minimum + actualRange!.maximum;
      actualRange!.maximum = actualRange!.minimum - actualRange!.maximum;
      actualRange!.minimum = actualRange!.minimum - actualRange!.maximum;
    }
    actualRange!.delta = actualRange!.maximum - actualRange!.minimum;

    actualRange!.interval =
        numericAxis.interval ?? calculateNumericNiceInterval(axisRenderer, actualRange!.delta, axisSize);
  }
}
